﻿// 文件使用MiniExcel源码， 使用时需遵循其 Apache License 2.0
// 修改摘要通知： 删除异步、Task等net2.0中不支持的内容，替换原先C#自带的zip处理方式，改用yacomp 进行解压缩。
using ExcelConfig.Exceptions;
using ExcelConfig.helpers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Globalization;

namespace ExcelConfig.Utils
{
    internal static class TypeHelper
    {
        internal static IEnumerable<IDictionary<string, object>> ConvertToEnumerableDictionary(IDataReader reader)
        {
            while (reader.Read())
            {
                var dic = new Dictionary<string, object>();
                for (var i = 0; i < reader.FieldCount; i++) dic[reader.GetName(i)] = reader.GetValue(i);
                yield return dic;
            }
        }

        /// <summary>
        /// From : https://stackoverflow.com/questions/906499/getting-type-t-from-ienumerablet
        /// </summary>
        internal static IEnumerable<Type> GetGenericIEnumerables(object o)
        {
            var types = new List<Type>();
            foreach (var typ in o.GetType()
                    .GetInterfaces())
                if (typ.IsGenericType
                        && typ.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                    types.Add(typ.GetGenericArguments()[0]);
            return types;
        }

        internal static bool IsNumericType(Type type, bool isNullableUnderlyingType = false)
        {
            if (isNullableUnderlyingType)
                type = Nullable.GetUnderlyingType(type) ?? type;
 
            switch (Type.GetTypeCode(type))
            {
                //case TypeCode.Byte:
                //case TypeCode.SByte:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.Decimal:
                case TypeCode.Double:
                case TypeCode.Single:
                    return true;
                default:
                    return false;
            }
        }

        internal static object TypeMapping<T>(T v, ExcelColumnInfo pInfo, object itemValue, int rowIndex, string startCell, Configuration config) where T : class, new()
        {
            try
            {
                return TypeMappingImpl(v, pInfo, itemValue, config);
            }
            catch (Exception ex) when (ex is InvalidCastException || ex is FormatException)
            {
                var columnName = pInfo.ExcelColumnName ?? pInfo.Property.Name;
                var startRowIndex = ReferenceHelper.ConvertCellToXY(startCell).Item2;
                var errorRow = startRowIndex + rowIndex + 1;
                
                var msg = $"ColumnName: {columnName}, CellRow: {errorRow}, Value: {itemValue}. The value cannot be cast to type {pInfo.Property.Info.PropertyType.Name}.";
                throw new ExcelInvalidCastException(columnName, errorRow, itemValue, pInfo.Property.Info.PropertyType, msg);
            }
        }

        private static object TypeMappingImpl<T>(T v, ExcelColumnInfo pInfo, object itemValue, Configuration config) where T : class, new()
        {
            object newValue = null;
            if (pInfo.Nullable && string.IsNullOrEmpty(itemValue?.ToString().Trim()))
            {
            }
            else if (pInfo.ExcludeNullableType == typeof(Guid))
            {
                newValue = new Guid(itemValue?.ToString() ?? Guid.Empty.ToString());
            }
            else if (pInfo.ExcludeNullableType == typeof(DateTimeOffset))
            {
                var vs = itemValue?.ToString();
                if (pInfo.ExcelFormat != null)
                {
                    if (DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value))
                    {
                        newValue = value;
                    }
                }
                else if (DateTimeOffset.TryParse(vs, config.Culture, DateTimeStyles.None, out var value))
                {
                    newValue = value;
                }
                else
                {
                    throw new InvalidCastException($"{vs} cannot be cast to DateTime");
                }
            }
            else if (pInfo.ExcludeNullableType == typeof(DateTime))
            {
                // fix issue 257 https://github.com/mini-software/MiniExcel/issues/257
                if (itemValue is DateTime)
                {
                    newValue = itemValue;
                    pInfo.Property.SetValue(v, newValue);
                    return newValue;
                }

                var vs = itemValue?.ToString();
                if (pInfo.ExcelFormat != null)
                {
                    if (pInfo.Property.Info.PropertyType == typeof(DateTimeOffset) && DateTimeOffset.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var offsetValue))
                    {
                        newValue = offsetValue;
                    }
                    else if (DateTime.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value))
                    {
                        newValue = value;
                    }
                }
                else if (DateTime.TryParse(vs, config.Culture, DateTimeStyles.None, out var dtValue))
                    newValue = dtValue;
                else if (DateTime.TryParseExact(vs, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dtExactValue))
                    newValue = dtExactValue;
                else if (double.TryParse(vs, NumberStyles.None, CultureInfo.InvariantCulture, out var doubleValue))
                    newValue = DateTime.FromOADate(doubleValue);
                else
                    throw new InvalidCastException($"{vs} cannot be cast to DateTime");
            }
#if NET6_0_OR_GREATER
            else if (pInfo.ExcludeNullableType == typeof(DateOnly))
            {
                if (itemValue is DateOnly)
                {
                    newValue = itemValue;
                    pInfo.Property.SetValue(v, newValue);
                    return newValue;
                }

                var vs = itemValue?.ToString();
                if (pInfo.ExcelFormat != null)
                {
                    if (DateOnly.TryParseExact(vs, pInfo.ExcelFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v))
                    {
                        newValue = _v;
                    }
                }
                else if (DateOnly.TryParse(vs, config.Culture, DateTimeStyles.None, out var _v))
                    newValue = _v;
                else if (DateOnly.TryParseExact(vs, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var _v2))
                    newValue = _v2;
                else if (double.TryParse(vs, NumberStyles.None, CultureInfo.InvariantCulture, out var _d))
                    newValue = DateOnly.FromDateTime(DateTime.FromOADate(_d));
                else
                    throw new InvalidCastException($"{vs} cannot be cast to DateOnly");                
            }
#endif
            else if (pInfo.ExcludeNullableType == typeof(TimeSpan))
            {
                if (itemValue is TimeSpan)
                {
                    newValue = itemValue;
                    pInfo.Property.SetValue(v, newValue);
                    return newValue;
                }

                var vs = itemValue?.ToString();
                if (pInfo.ExcelFormat != null)
                {
                    if (TimeSpan.TryParse(vs, out var value))
                    {
                        newValue = value;
                    }
                }
                else if (TimeSpan.TryParse(vs, out var tsValue))
                    newValue = tsValue;
                else if (TimeSpan.TryParse(vs, out var tsExactValue))
                    newValue = tsExactValue;
                else if (double.TryParse(vs, NumberStyles.None, CultureInfo.InvariantCulture, out var msValue))
                    newValue = TimeSpan.FromMilliseconds(msValue);
                else
                    throw new InvalidCastException($"{vs} cannot be cast to TimeSpan");
            }
            else if (pInfo.ExcludeNullableType == typeof(double)) // && (!Regex.IsMatch(itemValue.ToString(), @"^-?\d+(\.\d+)?([eE][-+]?\d+)?$") || itemValue.ToString().Trim().Equals("NaN")))
            {
                var invariantString = Convert.ToString(itemValue, CultureInfo.InvariantCulture);
                newValue = double.TryParse(invariantString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) ? value : double.NaN;
            }
            else if (pInfo.ExcludeNullableType == typeof(bool))
            {
                var vs = itemValue.ToString();
                if (vs == "1")
                    newValue = true;
                else if (vs == "0")
                    newValue = false;
                else
                    newValue = bool.Parse(vs);
            }
            else if (pInfo.Property.Info.PropertyType == typeof(string))
            {
                newValue = XmlEncoder.DecodeString(itemValue?.ToString());
            }
            else if (pInfo.ExcludeNullableType.IsEnum)
            {
                var fieldInfo =helper.SingleOrDefault(pInfo.ExcludeNullableType.GetFields(), e => AttributeExtension.GetAttribute<DescriptionAttribute>(e)?.Description == itemValue?.ToString());
                var value = fieldInfo?.Name ?? itemValue.ToString();
                newValue = Enum.Parse(pInfo.ExcludeNullableType, value, true);
            }
            else if (pInfo.ExcludeNullableType == typeof(Uri))
            {
                var rawValue = itemValue?.ToString();
                if (!Uri.TryCreate(rawValue, UriKind.RelativeOrAbsolute, out var uri))
                    throw new InvalidCastException($"Value \"{rawValue}\" cannot be converted to Uri");
                newValue = uri;
            }
            else
            {
                // Use pInfo.ExcludeNullableType to resolve : https://github.com/mini-software/MiniExcel/issues/138
                newValue = Convert.ChangeType(itemValue, pInfo.ExcludeNullableType, config.Culture);
            }

            pInfo.Property.SetValue(v, newValue);
            return newValue;
        }
    }
}
